<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Plugins\Network;

use Socket;
use Srv\Libs\Common\CommString;
use Srv\Libs\Common\CommTime;

class NetPing
{
    private $socketIcmp;                        // icmp协议socket
    private int $socketId       = 0;            // socketId
    private int $socketNum      = 0;            // socket次数

    /**
     * __construct
     */
    public function __construct()
    {
        !defined('MSG_WAITALL') && define('MSG_WAITALL', 0);
        !defined('AF_INET')     && define('AF_INET', 2);
        !defined('SOCK_RAW')    && define('SOCK_RAW', 3);
        !defined('SOL_SOCKET')  && define('SOL_SOCKET', 65535);
        !defined('SO_RCVTIMEO') && define('SO_RCVTIMEO', 4102);
    }

    /**
     * __destruct
     */
    public function __destruct()
    {
        if(is_resource($this->socketIcmp) || $this->socketIcmp instanceof Socket){
            @socket_close($this->socketIcmp);
            $this->socketIcmp   = null;
        }
    }

    /**
     * @param string $address
     * @param int $num
     * @param int $length
     * @param int $timeOut
     * @return array
     * get
     */
    public function get(string $address, int $num = 1, int $timeOut = 1, int $length = 1):array
    {
        $num            = max($num, 1);
        $timeOut        = min(max($timeOut, 1), 5);
        $length         = min(max($length, 1), 1024);
        ($length%2 !== 0) && ++$length;
        if(!(is_resource($this->socketIcmp) || $this->socketIcmp instanceof Socket)){   // ICMP(需root身份)
            $this->socketIcmp   = @socket_create(AF_INET, SOCK_RAW, getprotobyname('icmp'));
            if(!(is_resource($this->socketIcmp) || $this->socketIcmp instanceof Socket)) return [];
            @socket_set_block($this->socketIcmp);
        }
        if(!(is_resource($this->socketIcmp) || $this->socketIcmp instanceof Socket)) return [];
        if($this->socketId < 1 || $this->socketId >= 65535) $this->socketId = mt_rand(128, 1024);
        ++$this->socketId;
        @socket_set_option($this->socketIcmp, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $timeOut, 'usec' => 0]);
        @socket_set_option($this->socketIcmp, SOL_SOCKET, SO_SNDTIMEO, ['sec' => 0, 'usec' => 500000]);
        $result             = 1;
        $listData           = [];
        $addressIp          = @gethostbyname($address);
        if($addressIp){
            $bytes          = CommString::genRandStr($length);
            while(--$num >= 0){
                ++$this->socketNum;
                $tmpString  = $this->buildIcmpPack($bytes);
                @socket_sendto($this->socketIcmp, $tmpString, strlen($tmpString), 0, $addressIp, 0);
                $time       = CommTime::getTimeMicro();
                $reply      = @socket_read($this->socketIcmp, 65535, PHP_BINARY_READ);
                $time       = CommTime::diffTimeMicro($time);
                if(is_string($reply) && strlen($reply) > 15){
                    $ttl    = bindec(CommString::strToBin(substr($reply, 8, 1)));           // TTL
                    $to     = bindec(CommString::strToBin(substr($reply, 16, 1)));          // TO-IP[16-19]
                    $to     .= '.'.bindec(CommString::strToBin(substr($reply, 17, 1)));
                    $to     .= '.'.bindec(CommString::strToBin(substr($reply, 18, 1)));
                    $to     .= '.'.bindec(CommString::strToBin(substr($reply, 19, 1)));
                    $from   = bindec(CommString::strToBin(substr($reply, 12, 1)));          // FROM-IP[12-15]
                    $from   .= '.'.bindec(CommString::strToBin(substr($reply, 13, 1)));
                    $from   .= '.'.bindec(CommString::strToBin(substr($reply, 14, 1)));
                    $from   .= '.'.bindec(CommString::strToBin(substr($reply, 15, 1)));
                }else{
                    $ttl    = 0;
                    $to     = '0.0.0.0';
                    $from   = '0.0.0.0';
                }
                $listData[]  = [
                    'num'       => $this->socketNum,
                    'from'      => $from,
                    'to'        => $to,
                    'ttl'       => $ttl,
                    'time'      => $time,
                ];
            }
            @socket_set_nonblock($this->socketIcmp);
        }else{
            $result             = 2;    // HOST NOT FOUND
        }
        return [
            'result'    => $result,
            'address'   => $address,
            'addressIp' => $addressIp,
            'list'      => $listData,
        ];
    }

    /**
     * @param string $bytes
     * @return string
     * buildIcmpPack
     */
    private function buildIcmpPack(string $bytes):string
    {
        $type           = "\x08";           // 8 echo message; 0 echo reply message
        $code           = "\x00";           // always 0 for this program
        $checkSum       = "\x00\x00";       // generate checksum for icmp request
        $id             = pack('n1', ($this->socketId & 0xffff));   // we will have to work with this later
        $sqn            = pack('n1', ($this->socketNum & 0xffff));  // we will have to work with this later
        $checkSum       = $this->checksumIp($type.$code.$checkSum.$id.$sqn.$bytes);
        return $type.$code.$checkSum.$id.$sqn.$bytes;
    }

    /**
     * @param string $data
     * @return string
     * checksumIp
     */
    private function checksumIp(string $data):string
    {
        $sum        = array_sum(unpack('n*', $data));
        while($sum >> 16) $sum = ($sum & 0xffff) + ($sum >> 16);
        return pack('n1', ~$sum);
    }
}